Appearance
[中文版本](/posts/c教程/zh-CN/09-可移植与现代 C/086-Atomic Operations and Memory Models)
When multiple threads share data, you usually protect that data with locks likepthread_mutex_t. But sometimes, you need something faster, a way to perform an update that can’t be interrupted, even across threads. That’s where atomic operations come in.
This section introduces atomic operations in C and how the memory model ensures your program behaves predictably across cores.
Step 1. What Does “Atomic” Mean?
An atomic operation is one that happens all at once, it can’t be divided or interrupted.
Example idea: If two threads both runcounter++ at the same time:
- Without atomicity → race condition.
- With atomicity → one thread’s update completes fully before the other starts.
Atomic operations are essential in lock-free algorithms, concurrent queues, and reference counters.
Step 2. The Problem with counter++
This line looks simple:
counter++;But under the hood, it’s three separate steps:
- Load
counterfrom memory. - Increment it.
- Store it back.
Two threads doing this at once can lose updates:
Thread A: load(5)
Thread B: load(5)
Thread A: store(6)
Thread B: store(6)Result: one increment lost, final value should be 7 but ends up 6.
Step 3. Using Atomic Types
C11 introduced<stdatomic.h>, a portable way to use atomic operations.
#include <stdatomic.h>
#include <stdio.h>
int main(void) {
atomic_int counter = 0;
atomic_fetch_add(&counter, 1);
atomic_fetch_add(&counter, 1);
printf("%d\n", counter); // 2
}No locks. No race conditions. Theatomic_* functions guarantee the operations are atomic at the hardware level.
Step 4. Common Atomic Functions
| Function | Description |
|---|---|
atomic_load | Read atomically |
atomic_store | Write atomically |
atomic_fetch_add | Add and return old value |
atomic_fetch_sub | Subtract and return old value |
atomic_exchange | Replace and return old value |
atomic_compare_exchange_strong | Compare-and-swap |
Example:
atomic_compare_exchange_strong(&counter, &expected, desired);Ifcounter == expected, replace it withdesired. Otherwise, updateexpected with the current value.
Step 5. Tiny Code: Atomic Counter with Threads
#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
atomic_int counter = 0;
void* work(void* arg) {
for (int i = 0; i < 100000; i++)
atomic_fetch_add(&counter, 1);
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, work, NULL);
pthread_create(&t2, NULL, work, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Counter = %d\n", counter);
}Output is always200000, with no mutexes and no data races.
Step 6. Relaxed vs Sequential Consistency
Atomic operations can have different memory orders. By default, they’re sequentially consistent, the strongest and safest ordering.
| Memory Order | Meaning |
|---|---|
memory_order_seq_cst | Global consistent order (default) |
memory_order_relaxed | Only atomicity guaranteed |
memory_order_acquire | Prevent reordering before load |
memory_order_release | Prevent reordering after store |
memory_order_acq_rel | Acquire + Release combo |
Example:
atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);This is faster but weaker, use only when you understand your memory model.
Step 7. Memory Barriers and Visibility
Modern CPUs reorder reads/writes for performance. Atomics, fences, and locks control when updates become visible to other threads.
Example: Thread A writesready = 1. Thread B waits until it seesready == 1. If the compiler reorders memory operations, Thread B might not see the change.
Use:
atomic_thread_fence(memory_order_seq_cst);to prevent reordering across the fence.
Step 8. Compare-and-Swap (CAS)
CAS is the backbone of lock-free data structures.
int expected = 0;
int desired = 1;
if (atomic_compare_exchange_strong(&counter, &expected, desired)) {
printf("Swapped!\n");
}It atomically checks ifcounter == expected and updates it, all in one instruction. This is used to build things like spinlocks, queues, and reference counters.
Step 9. Spinlocks with Atomics
A spinlock keeps checking until it can acquire the lock.
#include <stdatomic.h>
#include <unistd.h>
atomic_flag lock = ATOMIC_FLAG_INIT;
void lock_spin(void) {
while (atomic_flag_test_and_set(&lock))
; // busy wait
}
void unlock_spin(void) {
atomic_flag_clear(&lock);
}This is efficient when the lock is held for a very short time. For longer waits, usepthread_mutex_t instead.
Step 10. Tiny Code: Atomic Reference Counter
#include <stdatomic.h>
#include <stdio.h>
typedef struct {
atomic_int refcount;
} Object;
void retain(Object* obj) {
atomic_fetch_add(&obj->refcount, 1);
}
void release(Object* obj) {
if (atomic_fetch_sub(&obj->refcount, 1) == 1)
printf("Object freed\n");
}
int main(void) {
Object obj = { .refcount = 1 };
retain(&obj);
release(&obj);
release(&obj);
}Output:
Object freedThis is how many real-world systems (e.g. file handles, shared memory) track usage.
Why It Matters
Atomic operations are the building blocks of lock-free programming. They allow you to write high-performance concurrent code without blocking other threads. The C memory model gives you guarantees to reason about correctness even across multiple CPU cores.
Try It Yourself
- Replace a mutex counter with an atomic counter.
- Implement a spinlock using
atomic_flag. - Use
atomic_compare_exchange_strongto build a simple CAS loop. - Test the difference between
memory_order_relaxedandseq_cst. - Build a reference-counted structure using atomics.
Next, you’ll explore using C with other languages (FFI), how to make C libraries callable from Python, Rust, and Go.
